﻿#region Using's

using System;
using System.CodeDom.Compiler;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CSharp;
using MyUtils;

#endregion

namespace WUnit.Framework
{
	public abstract class FixtureWUnit : IDisposable
	{
		const string objectEqualsWarning =
			"Do not call Equals and ReferenceEquals in fixtures. This is very ambigious with asserts. Use full syntax insted: Object.Equals, Object.ReferenceEquals";

		readonly aspect _aspect;
		bool disposed;
		List<TestResult> results = new List<TestResult>();

		protected FixtureWUnit()
		{
			_aspect = new aspect(this);
		}

		[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "value")]
		[SuppressMessage("Microsoft.Design", "CA1044:PropertiesShouldNotBeWriteOnly")]
		[SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores")]
		[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "_")]
		//[CLSCompliant(false)]
		protected static object _
		{
			set { }
		}

		#region IDisposable Members

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		#endregion

		public event EventHandler AssertCalling = delegate { };
		public event EventHandler AssertCalled = delegate { };

		/*		public void Setup() {
			DateTimeNow.Stub(DateTime.UtcNow);
		}*/

		[DebuggerNonUserCode]
		public void Teardown()
		{
			try
			{
				Func<TestResult, string> testInfoSelector = x => "" + (x.Success ? "V" : "-") + " \t" + x.Message;
				//—―   " V " : "—"
				var all = results.Select(testInfoSelector).Join(Environment.NewLine);
				var fails = results.Where(x => !x.Success).Select(testInfoSelector).Join(Environment.NewLine);

				int allcnt = results.Count, succcnt = results.Where(x => x.Success).Count();
				var dispSimbsMax = 50;
				var perc = succcnt / (double)allcnt;
				var dispSimbs = (int)Math.Round(perc * dispSimbsMax);

				if (dispSimbs < 0)
				{
					dispSimbs = 0;
				}

				Console.WriteLine("{0:0.#%} [{1}{2}]".Arg(perc, new string('#', dispSimbs),
																	  new string('_', dispSimbsMax - dispSimbs)));
				Console.WriteLine();
				Console.WriteLine(all);

				if (results.Any(x => !x.Success))
				{
					throw new TestAssertionException(fails);
				}
				if (results.Any(x => x.Message.StartsWith("Inconclusive", StringComparison.OrdinalIgnoreCase)))
				{
					throwInconclusive(all);
				}
			}
			finally
			{
				results = new List<TestResult>();
			}
		}

		static void throwInconclusive(string value)
		{
			throw new InconclusiveTestAssertionException("Inconclusive\r\n" + value);
		}

		void result(bool succ, string msg)
		{
			results.Add(new TestResult
			{
				Success = succ,
				Message = msg
			});
		}

		public void True(bool condition)
		{
			using (_aspect.Run)
			{
				result(condition, "True({0})".Arg(condition));
			}
		}

		public void False(bool condition)
		{
			using (_aspect.Run)
			{
				result(!condition, "False({0})".Arg(condition));
			}
		}

		public bool Equal<T>(T arg1, T arg2)
		{
			using (_aspect.Run)
			{
				var valid = object.Equals(arg1, arg2);
				var msg = "\t";
				var argtype = argtype<T>(arg1, arg2);

				if (argtype == typeof(double))
				{
					var d1 = ((IConvertible)arg1).ToDouble(null);
					var d2 = ((IConvertible)arg2).ToDouble(null);
					var dif = Math.Abs(d1 - d2);
					valid = d1.Equals(d2) || dif < .000000001;
					if (valid && dif > 0)
					{
						msg += "Tolerance to {0:0.00E+0}. ".Arg(dif.ConvertTo<double>());
					}
					else if (dif > 0)
					{
						msg += "Difference {0}. ".Arg(dif);
					}
				}
				else if (argtype == typeof(DateTime))
				{
					var d1 = (DateTime)(object)arg1;
					var d2 = (DateTime)(object)arg2;
					var dif = new TimeSpan(Math.Abs(d1.Ticks - d2.Ticks));
					valid = dif < TimeSpan.FromSeconds(2);
					if (valid && dif.Exists())
					{
						msg += "Tolerance to {0}. ".Arg(dif.ToSpanString());
					}
					else if (dif.Exists())
					{
						msg += "Difference {0}. ".Arg(dif.ToSpanString());
					}
				}
				else if (argtype == typeof(TimeSpan))
				{
					var d1 = (TimeSpan)(object)arg1;
					var d2 = (TimeSpan)(object)arg2;
					var dif = new TimeSpan(Math.Abs(d1.Ticks - d2.Ticks));
					valid = dif < TimeSpan.FromSeconds(2);
					if (valid && dif.Exists())
					{
						msg += "Tolerance to {0}. ".Arg(dif.ToSpanString());
					}
					else if (dif.Exists())
					{
						msg += "Difference {0}. ".Arg(dif.ToSpanString());
					}
				}

				var describeType = argtype == typeof(object);

				var genericType = typeof(T).Name;
				if (!describeType && genericType != argtype.Name)
					genericType = "{0}".Arg(argtype.Name);

				var com1 = toString(arg1, describeType);
				var com2 = toString(arg2, describeType);
				var quotes = "";
				if (com1.Contains(',') || com2.Contains(','))
				{
					quotes = "\"";
				}
				result(valid,
							 "Equal<{0}>({4}{1}{4}, {4}{2}{4}) {3}".ArgI(genericType, com1, com2, msg, quotes));
				return valid;
			}
		}

		static Type argtype<T>(object arg1, object arg2)
		{
			if (typeof(T) != typeof(object))
			{
				return typeof(T);
			}

			if (!object.ReferenceEquals(arg1, null) && !object.ReferenceEquals(arg2, null)
				 && arg1.GetType() == arg2.GetType())
			{
				return arg1.GetType();
			}

			return typeof(object);
		}

		// Workaround of invalid inferring
		public void Equal<T>(IEnumerable<T> expected, IEnumerable<T> actual)
		{
			Equal(expected, actual as IEnumerable);
		}

		public void Equal(IEnumerable expected, IEnumerable actual)
		{
			using (_aspect.Run)
			{
				const string logo = "Equal(IEnumerable, IEnumerable)";
				if (expected == null)
				{
					result(false, logo + ": expected enumerator is null");
					return;
				}
				if (actual == null)
				{
					result(false, logo + ": actual enumerator is null");
					return;
				}
				var expEtor = expected.GetEnumerator();
				var actEtor = actual.GetEnumerator();

				//var success = true;
				for (var c = 0; ; c++)
				{
					var expAnyMore = expEtor.MoveNext();
					var actAnyMore = actEtor.MoveNext();
					if (!expAnyMore && !actAnyMore)
					{
						// simultaniously finish
						result(true, logo);
						return;
					}
					else if (expAnyMore && actAnyMore)
					{
						// checking element
						var a = actEtor.Current;
						var b = expEtor.Current;
						if (!object.Equals(a, b))
						{
							var useMultiLine = a.ToString().Length > 6;
							result(false,
									 logo
									 +
									 ": Element at index {0} do not match: {3}{4}Expected: {4}`{1}` {3}{4}Actual:       {4}`{2}`".Arg
										(c, toString(b), toString(a), useMultiLine ? "\r\n" : null, useMultiLine ? "\t" : null));
							return;
						}
					}
					else
					{
						// different length
						result(false,
								 logo
								 +
								 ": {0} sequence is shorter: {1} elements. {3}: {2}".Arg(expAnyMore ? "Actual" : "Expected", c,
																											toString(expAnyMore
																															? expEtor.Current
																															: actEtor.Current),
																											expAnyMore ? "Missing" : "Surplus"));
						return;
					}
				}
			}
		}

		//		public void Equivalent<T>(IEnumerable<T> expected, IEnumerable<T> actual) {
		//			Equivalent(expected, actual, "<" + typeof (T).Name + ">");
		//		}

		//		public void Equivalent(IEnumerable expected, IEnumerable actual) {
		//			Equivalent(expected, actual, null);
		//		}

		public void Equivalent<T>(IEnumerable<T> expected, IEnumerable<T> actual)
		{
			using (_aspect.Run)
			{
				var logo = "Equivalent<{0}>(IEnumerable<>, IEnumerable<>)".Arg(typeof(T).Name);
				if (expected == null)
				{
					result(false, logo + ": expected enumerator is null");
					return;
				}
				if (actual == null)
				{
					result(false, logo + ": actual enumerator is null");
					return;
				}

				var actualCache = actual.ToList();

				var actualCacheCount = actualCache.Count;

				bool sorted;
				try
				{
					actualCache.Sort();
					sorted = true;
				}
				catch (Exception ex)
				{
					throw new TestRunException("TODO: Catch specific exceptions", ex);
					//sorted = false;
				}
				var expectedCache = expected.ToArray();
				var missings = new List<T>();
				foreach (var item in expectedCache)
				{
					// ReSharper disable ConditionIsAlwaysTrueOrFalse
					var i = sorted ? actualCache.BinarySearch(item) : actualCache.IndexOf(item);
					// ReSharper restore ConditionIsAlwaysTrueOrFalse
					if (i == -1)
					{
						missings.Add(item);
					}
					else
					{
						actualCache.RemoveAt(i);
					}
				}
				var surpluses = actualCache;

				if (surpluses.Count == 0 && missings.Count == 0)
				{
					result(true, logo);
				}
				else
				{
					result(false,
							 logo
							 +
							 ": Expected {0} elements. Actual {1}. Missing {2}. Surplus {3}.".Arg(expectedCache.Length,
																														 actualCacheCount,
																														 toStringCollectionIfNotEmpty(
																															missings),
																														 toStringCollectionIfNotEmpty(
																															surpluses)));
				}
			}
		}

		static string toStringCollectionIfNotEmpty<T>(ICollection<T> col)
		{
			var maxToDisp = 10;
			return "{0} elements".Arg(col.Count)
					 +
					 (col.Count > 0
						? ": [ " + col.Take(maxToDisp).Select(x => toString(x, typeof(T) == typeof(object))).Join(", ")
						  + (col.Count > maxToDisp ? ", .." : null) + " ]"
						: null);
		}

		static string toString(object p)
		{
			return toString(p, true);
		}

		static string toString(object p, bool describeType)
		{
			if (p == null)
			{
				return "null";
			}
			var type = p.GetType();
			if (type.IsValueType && !type.IsPrimitive && !p.Exists())
			{
				return "default" + (describeType ? "({0})".Arg(p.GetType().Name) : null);
			}
			if (p is DateTime)
			{
				return ((DateTime)p).ToStringFull(); // ("yyyy-MM-dd H:mm ");
			}
			if (p is TimeSpan)
			{
				return ((TimeSpan)p).ToSpanString();
			}

			var tostr = p.ToString();

			if (tostr == p.GetType().FullName || string.IsNullOrEmpty(tostr.Trim()))
			{
				var pros = p.GetType().GetProperties().Where(x => x.CanRead && x.GetIndexParameters().Length == 0);
				if (pros.Any())
				{
					var pro1 = pros.First();
					tostr = pro1.Name + "=" + pro1.GetValue(p, null);
				}
				else
				{
					var fields = p.GetType().GetFields();
					if (fields.Any())
					{
						var field1 = fields.First();
						tostr = field1.Name + "=" + field1.GetValue(p);
					}
				}
			}

			return describeType ? "{0}: {1}".Arg(type.Name, p) : tostr;
		}

		public override int GetHashCode()
		{
			return base.GetHashCode();
		}

#pragma warning disable 0809 // Obsolete member overrides non-obsolete member.
		[SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")]
		[Obsolete(objectEqualsWarning, true)]
		[EditorBrowsable(EditorBrowsableState.Never)]
		public override bool Equals(object obj)
		{
			throw new TestRunException(objectEqualsWarning);
		}
#pragma warning restore 0809

		[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "objB")]
		[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "objA")]
		[Obsolete(objectEqualsWarning, true)]
		[EditorBrowsable(EditorBrowsableState.Never)]
		public new static bool Equals(object objA, object objB)
		{
			throw new TestRunException(objectEqualsWarning);
		}

		[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "objB")]
		[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "objA")]
		[Obsolete(objectEqualsWarning, true)]
		[EditorBrowsable(EditorBrowsableState.Never)]
		public new static bool ReferenceEquals(object objA, object objB)
		{
			throw new TestRunException(objectEqualsWarning);
		}

		public void Greater<T>(T arg1, T arg2) where T : IComparable<T>
		{
			using (_aspect.Run)
			{
				bool valid;
				string msg = null;

				if (!object.ReferenceEquals(arg1, null) && !object.ReferenceEquals(arg2, null))
				{
					valid = arg1.CompareTo(arg2) == 1;
				}
				else
				{
					valid = false;
					msg = "Argument is null";
				}

				result(valid,
						 "Greater<{3}>({0}, {1})\t{2}".ArgI(toString(arg1, false), toString(arg2, false), msg,
																		typeof(T).Name));
			}
		}

		public Exception Throws(Action act)
		{
			return Throws(act, null);
		}

		public Exception Throws(Action act, string msg)
		{
			using (_aspect.Run)
			{
				var ex = RecordException(act);
				var throwed = ex != null;

				results.Add(new TestResult
				{
					Success = ex != null,
					Message = "Throws [" + msg + "] " + (throwed ? " `{0}`".Arg(ex.Message) : null),
				});
				return ex;
			}
		}

		public Exception Throws<T>(Action act)
		{
			return Throws<T>(act, null);
		}

		[SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
		public Exception Throws<T>(Action act, string msg)
		{
			using (_aspect.Run)
			{
				var ex = RecordException(act);
				var throwed = ex != null;
				var valid = throwed && ex.GetType() == typeof(T);

				results.Add(new TestResult
				{
					Success = valid,
					Message =
									"Throws<{0}> [{1}]".Arg(typeof(T).Name, msg)
									+ (throwed && !valid ? " actually throw <{0}>".Arg(ex.GetType().Name) : null)
									+ (throwed ? " `{0}`".Arg(ex.Message) : null),
				});
				return ex;
			}
		}

		public Exception NotThrows(Action act)
		{
			return NotThrows(act, null);
		}

		public Exception NotThrows(Action act, string msg)
		{
			using (_aspect.Run)
			{
				var ex = RecordException(act);
				result(ex == null, (ex == null ? "Not Throws" : ex.Message) + ". [" + msg + "]");
				return ex;
			}
		}

		[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		public static Exception RecordException(Action act)
		{
			try
			{
				act();
				return null;
			}
			catch (Exception ex)
			{
				return ex;
			}
		}

		public void Match(string pattern, string value)
		{
			using (_aspect.Run)
			{
				result(Regex.IsMatch(value, pattern), "IsMatch({0},{1})".Arg(pattern, value));
			}
		}

		[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
		public void Inconclusive(string msg)
		{
			//throwInconclusive(msg);
			using (_aspect.Run)
			{
				result(true, "Inconclusive. " + msg);
			}
		}

		[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
		public void Inconclusive()
		{
			//throwInconclusive(null);
			using (_aspect.Run)
			{
				result(true, "Inconclusive");
			}
		}

		public void Wait(Func<bool> condition)
		{
			Wait(5000, condition);
		}

		public void Wait(int ms, Func<bool> condition)
		{
			using (_aspect.Run)
			{
				var logo = "Wait({0}, func)".Arg(TimeSpan.FromMilliseconds(ms).ToSpanString());
				var sw = Stopwatch.StartNew();
				var ok = false;
				while (sw.ElapsedMilliseconds < ms)
				{
					if (condition())
					{
						ok = true;
						break;
					}
				}
				result(ok, logo + "\tElapsed: " + sw.Elapsed.ToSpanString());
			}
		}

		[SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_aspect")]
		protected virtual void Dispose(bool disposeManaged)
		{
			if (disposed)
			{
				return;
			}
			disposed = true;
			if (disposeManaged) { }
		}

		#region Nested type: aspect

		sealed class aspect : IDisposable
		{
			readonly FixtureWUnit _flowAsserts;

			public aspect(FixtureWUnit flowAsserts)
			{
				_flowAsserts = flowAsserts;
			}

			public IDisposable Run
			{
				get
				{
					_flowAsserts.AssertCalling(null, EventArgs.Empty);
					return this;
				}
			}

			#region IDisposable Members

			public void Dispose()
			{
				GC.SuppressFinalize(this);
				_flowAsserts.AssertCalled(null, EventArgs.Empty);
			}

			#endregion
		}

		#endregion

		#region Nested type: TestResult

		class TestResult
		{
			public string Message;
			public bool Success;
		}

		#endregion

		protected Assembly CompiledAssembly;

		public bool IsCompilableInsideMethod(string code)
		{
			var compiler = new CSharpCodeProvider(new Dictionary<string, string> {{"CompilerVersion", "v3.5"}});
			var options = new CompilerParameters {GenerateInMemory = true};
			var callingAssembly = Assembly.GetCallingAssembly();
			//Trace.WriteLine("ca: " + callingAssembly.FullName);
			// var callingAssembly = new StackTrace(1).GetFrame(0).GetMethod().DeclaringType.Assembly;
			foreach (var assemblyName in callingAssembly.GetReferencedAssemblies()
				.Where(x => x.Name != "Microsoft.VisualStudio.QualityTools.UnitTestFramework"))
			{
				options.ReferencedAssemblies.Add(assemblyName.Name + ".dll");
				Trace.WriteLine("ref: " + assemblyName.Name);
			}

			var stringBuilder = new StringBuilder();
			stringBuilder.AppendLine(
				@"
public static class CompileTestClass
{{
	public static object CompileTestMethod()
	{{
		{0}
		return null;
	}}
}}"
					.ArgI(code));
			code = stringBuilder.ToString();
			// code = string.Join("", code.SplitToLines().Select((x, i) => "/* " + (i + 1).ToString("00") + " */ " + x.Trim('\n') + Environment.NewLine).ToArray());
			Trace.WriteLine(code);
			var result1 = compiler.CompileAssemblyFromSource(options, code);
			foreach (var error in result1.Errors)
			{
				Trace.WriteLine(error);
			}
			var res = result1.Errors.Count == 0;

			CompiledAssembly = res ? result1.CompiledAssembly : null;
			return res;
		}
	}
}